其他
看雪·深信服 2021 KCTF 春季赛 | 第五题设计思路及解析
武林大会在即,看雪er们跟随武林少才的脚步齐聚华山,开启第五题《华山论剑》,一共有 4 支战队完成目标。
xtgo战队当仁不让,率先拿下本题,仅用时52092秒。 hzqmwne战队和雨落星沉战队紧追不舍,很快也成功攻破此题。
目前赛程已经过半,我们来看看目前场上的状况吧!攻击方排名前10如下:
大家在赛场上你追我赶,好不热闹!接下来我们一起来看看本题的“通关宝典”吧!
出题团队简介
本题出题战队为 ArmVMP :
主攻方向基于二进制方案的文件及虚拟机保护,覆盖行业包括安卓,ios平台,及物联网平台,致力于通过技术手段,以减少对用户的恶意攻击及破坏行为。
专家点评
看雪专家netwind点评:该题在反调试方面采用了指令虚拟化的方式来干扰攻击方进行代码逻辑分析,在算法方面采用改动过的SHA1算法和RC4算法来保护序列号;攻击方需要在识别出指令虚拟化逻辑以及加密算法的修改逻辑的基础上才能很好的完成此题;作为安卓平台的题目,难度中等偏上,能够完成此题证明攻防双方都具备较强的实力!
赛题设计思路
算法简单,可玩性高,有兴趣可以随时交流。
规则2的demo为:KCTF-2.sign.apk
其中规则2的两组序列号如下:
serial:7C9815255BFE832D3F93140B
* name:KCTF
serial:17726331DA0fE737149c8202
设计思路
2. hash值做明文和Java_com_example_hellojni_HelloJni_stringFromJNI地址做密钥K参与rc4运算(稍作修改),其中hash前12字节与密钥流按位做异或并与hash后12字节按位相加得到新的12字节串值。
3. 将新得到的十六进制12字节串转换为ASCII码形式即为24字节的serial值。
保护方法
解题思路
赛题解析
本赛题解析由看雪论坛 mb_mgodlfyn 给出:
先尝试用ida在真机上调试,体验很不好(最主要的问题是,0x5000处的汇编指令ida识别为"BL LR, #0xBA ",但是单步调试时无法进入这条指令内部,不知道原因) 考虑到这个so文件接口不复杂,于是写了一个简单的loader在linux上直接加载(按相对偏移mmap所有的load segment;自己定义一些dummy函数填入got表和JNIEnv.functions表),然后gdb本地调试,体验非常好(方便随时重启;配合插件能看多级指针)(另:0x5000处的汇编在gdb里识别为"ldr lr, [sp], #4",和ida里不一样,而且能单步调试)(但是有一个坑,不能下数据断点(rwatch/watch),让后续分析变得麻烦) loader的代码:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <assert.h>
struct str {
const char *s;
int len;
};
//struct str global_name = {.s = "ed8b9244350d3644", .len = 16};
//struct str global_serial = {.s = "7C9815255BFE832D3F93140B", .len = 24};
struct str global_name = {.s = "KCTF", .len = 4};
struct str global_serial = {.s = "17726331DA0FE737149C8202", .len = 24};
//struct str global_serial = {.s = "17726331da0fe737149c8202", .len = 24};
struct JNINativeInterface_ {
unsigned int f[0x1000/4];
};
typedef struct JNIEnv_ {
struct JNINativeInterface_ *functions;
} JNIEnv;
void JNICALL_FindClass(JNIEnv *env, const char *name) {
printf("JNICALL_FindClass %s\n", name);
}
void JNICALL_NewStringUTF(JNIEnv *env, const char *utf) {
printf("%s %s\n", __func__, utf);
}
int JNICALL_GetMethodID(JNIEnv *env, void *clazz, const char *name, const char *sig) {
printf("%s %p %s %s\n", __func__, clazz, name, sig);
return 0x55555501;
}
void *JNICALL_CallObjectMethod(JNIEnv *env, void *obj, int methodID) {
assert(methodID == 0x55555501);
printf("%s %p %x\n", __func__, obj, methodID);
return obj;
}
int JNICALL_GetArrayLength(JNIEnv *env, struct str *array) {
printf("%s %p\n", __func__, array);
return array->len;
}
unsigned char *JNICALL_GetByteArrayElements(JNIEnv *env, struct str *array, int isCopy) {
printf("%s %p %d\n", __func__, array, isCopy);
return array->s;
}
void JNICALL_ReleaseByteArrayElements(JNIEnv *env, void *array, void *elems, int mode) {
printf("%s %p %p %d\n", __func__, array, elems, mode);
}
void *got_malloc(int size) {
void *r = malloc(size);
printf("%s %d %p\n", __func__, size, r);
return r;
}
void got_free(void *p) {
printf("%s\n", __func__);
free(p);
}
void got_memset(char *p, int n, int count) {
printf("%s %p %d %d\n", __func__, p, n, count);
memset(p, n, count);
}
void bp(void) {
;
}
void stack_chk_guard(void) {
printf("%s\n", __func__);
}
void imp___gnu_Unwind_Find_exidx(void) {
printf("%s\n", __func__);
}
void cxa_call_unexpected(void) {
printf("%s\n", __func__);
}
int main(void) {
int fd = open("libhello-jni.so", O_RDONLY);
unsigned char *fmem = mmap(NULL, 0x7000, PROT_READ, MAP_PRIVATE, fd, 0);
unsigned char *mem = mmap(0xdead0000, 0x8000, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
memcpy(mem, fmem, 0x2b13);
memcpy(mem+0x3e8c, fmem+0x2e8c, 0x1a8);
memcpy(mem+0x5000, fmem+0x4000, 0x26bc);
munmap(fmem, 0x7000);
close(fd);
*(unsigned int *)(mem+0x3f98) = mem+0x1034+1; // xxxxxxxxxx2+1
*(unsigned int *)(mem+0x3f9c) = stack_chk_guard;
*(unsigned int *)(mem+0x3fb8) = imp___gnu_Unwind_Find_exidx;
*(unsigned int *)(mem+0x3fc4) = cxa_call_unexpected;
*(unsigned int *)(mem+0x3fc8) = mem+0x3f98; // _GLOBAL_OFFSET_TABLE_@got
*(unsigned int *)(mem+0x3fa0) = mem+0x10e4+1; // Java_com_example_hellojni_HelloJni_stringFromJNI_ptr+1
*(unsigned int *)(mem+0x3fa4) = mem+0x4004; // f_data_key_dllink
*(unsigned int *)(mem+0x3fa8) = mem+0x401c; // f_sucess
*(unsigned int *)(mem+0x3fdc) = got_malloc; // malloc@got
*(unsigned int *)(mem+0x3fe0) = got_memset; // memset@got
*(unsigned int *)(mem+0x3fe4) = got_free; // free@got
for (int i = 0x3f98; i < 0x4000; i+=4) {
if (*(unsigned int *)(mem+i) == 0) {
//*(unsigned int *)(0x77770000+i) = i;
}
}
struct JNINativeInterface_ jnii = {.f = {0}};
for(int i = 0; i < 0x100; i++) {
jnii.f[i] = 0x11110000+i*4;
}
jnii.f[0x18/4] = JNICALL_FindClass;
jnii.f[0x29c/4] = JNICALL_NewStringUTF;
jnii.f[0x84/4] = JNICALL_GetMethodID;
jnii.f[0x88/4] = JNICALL_CallObjectMethod;
jnii.f[0x2ac/4] = JNICALL_GetArrayLength;
jnii.f[0x2e0/4] = JNICALL_GetByteArrayElements;
jnii.f[0x300/4] = JNICALL_ReleaseByteArrayElements;
JNIEnv env;
env.functions = &jnii;
printf("&global_name: %p, &global_serial: %p\n", &global_name, &global_serial);
bp();
((void (*)(int, int, int, int, int))(mem+0x10e4+1))(&env, 0xaaaa, &global_name, &global_serial, 0xdddd);
return 0;
}
编译:arm-linux-gnueabi-gcc loader.c -g -mthumb -o a.out
运行:qemu-arm -L /usr/arm-linux-gnueabi/ -g 1234 ./a.out
调试:gdb-multiarch -ex "file ./a.out" -ex "target remote localhost:1234" (在Ubuntu上可以apt-get install gcc-arm-linux-gnueabi libc6-armel-cross gdb-multiarch 安装依赖) 题目是类似vmp的虚拟机。 虚拟机指令的结构:以0x5d04处为例:
从thumb指令开始(0x5d04),通过一个B跳转跳过1或2个dword(0x5d06和0x5d08)到后面的arm指令(0x5d10),先是 BX PC ,然后是 STR PC, [SP,#var_FC] 把PC放入栈,最后B跳转到一个外部函数(0x7270),外部函数返回到下一条指令。
外部函数大部分以push所有寄存器开始,以pop所有寄存器结束,通过栈上保存的PC向前找参数(0x5d06和0x5d08),返回到下一条指令(0x5d1c)。
LOAD:00005D04 ; ---------------------------------------------------------------------------
LOAD:00005D04 CODE16
LOAD:00005D04 B sub_5D10 ; Branch
LOAD:00005D04 ; ---------------------------------------------------------------------------
LOAD:00005D06 CODE32
LOAD:00005D06 DCW 0xBF00
LOAD:00005D08 DCD 8, 0x30D00
LOAD:00005D10 CODE16
LOAD:00005D10
LOAD:00005D10 ; =============== S U B R O U T I N E =======================================
LOAD:00005D10
LOAD:00005D10 ; Attributes: thunk
LOAD:00005D10
LOAD:00005D10 sub_5D10 ; CODE XREF: LOAD:00005D04↑j
LOAD:00005D10 BX PC ; Branch to/from Thumb mode
LOAD:00005D10 ; ---------------------------------------------------------------------------
LOAD:00005D12 DCB 1
LOAD:00005D13 DCB 0
LOAD:00005D13 ; End of function sub_5D10
LOAD:00005D13
LOAD:00005D14 CODE32
LOAD:00005D14
LOAD:00005D14 ; =============== S U B R O U T I N E =======================================
LOAD:00005D14
LOAD:00005D14
LOAD:00005D14 sub_5D14 ; CODE XREF: sub_5D10↑j
LOAD:00005D14
LOAD:00005D14 var_FC = -0xFC
LOAD:00005D14
LOAD:00005D14 STR PC, [SP,#var_FC] ; Store to Memory
LOAD:00005D18 B sub_7270 ; Branch
LOAD:00005D18 ; End of function sub_5D14
LOAD:00005D1C ; ---------------------------------------------------------------------------
LOAD:00005D1C CODE16
LOAD:00005D1C B sub_5D28 ; Branch
LOAD:00005D1C ; ---------------------------------------------------------------------------
LOAD:00005D1E CODE32
几个关键的位置:
* 所有的内存读取:0x72fc、0x7304、0x730c* 所有的内存写入:0x71f4、0x71fc、0x7204* 比较(cmp):0x75ac* 加法:0x7644在这8个位置下断点,基本上就能看出程序的完整流程,不需要分析虚拟机指令:* 分配并初始化若干个缓冲区* 对name做一些运算* 对serial做hexdecode(逐字符调用程序里的 sub_DDA 函数,这个函数没有混淆)* 初始化RC4的sbox(0x6054附近,循环)* 计算RC4(0x638c的附近,循环)前期调试分析过程很漫长,但最终找出serial很简单:
先在 0x638c 下断点,运行到这里后在 0x71f4 下断点,可以发现每计算出一个加密值,就会存入另一个缓冲区中,且这个值与serial做hexdecode之后的值是相等的。所以,只需要把name初始化为"KCTF",就可以在这里提取出正确的serial。 name:KCTF
serial:17726331DA0FE737149C8202 hexdecode没有区分大小写,因此serial的字母大小写可以替换,造成多解。
往期解析
1. 看雪·深信服 2021 KCTF 春季赛 | 第二题设计思路及解析
2. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析3. 看雪·深信服 2021 KCTF 春季赛 | 第三题设计思路及解析4. 看雪·深信服 2021 KCTF 春季赛 | 第四题设计思路及解析主办方
看雪CTF(简称KCTF)是圈内知名度最高的技术竞技之一,从原CrackMe攻防大赛中发展而来,采取线上PK的方式,规则设置严格周全,题目涵盖Windows、Android、iOS、Pwn、智能设备、Web等众多领域。
看雪CTF比赛历史悠久、影响广泛。自2007年以来,看雪已经举办十多个比赛,与包括金山、360、腾讯、阿里等在内的各大公司共同合作举办赛事。比赛吸引了国内一大批安全人士的广泛关注,历年来CTF中人才辈出,汇聚了来自国内众多安全人才,高手对决,精彩异常,成为安全圈的一次比赛盛宴,突出了看雪论坛复合型人才多的优势,成为企业挑选人才的重要途径,在社会安全事业发展中产生了巨大的影响力。
合作伙伴
深信服科技股份有限公司成立于2000年,是一家专注于企业级安全、云计算及基础架构的产品和服务供应商,致力于让用户的IT更简单、更安全、更有价值。目前深信服在全球设有50余个分支机构,员工规模超过7000名。
第六题正在火热进行中,
👆还在等什么,快来参赛吧!- End -
球分享
球点赞
球在看